﻿using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace NetJsWire.Lite.Helpers
{
    internal sealed class DynamicProxyMetaObject<T> : DynamicMetaObject
    {
        private readonly DynamicProxy<T> _proxy;
        private readonly bool _dontFallbackFirst;

        internal DynamicProxyMetaObject( Expression expression, T value, DynamicProxy<T> proxy, bool dontFallbackFirst )
            : base( expression, BindingRestrictions.Empty, value )
        {
            _proxy = proxy;
            _dontFallbackFirst = dontFallbackFirst;
        }

        private new T Value { get { return ( T ) base.Value; } }

        private bool IsOverridden( string method )
        {
            return _proxy.GetType().GetMember( method, MemberTypes.Method, BindingFlags.Public | BindingFlags.Instance ).Cast<MethodInfo>()
              .Any( info =>
                  // check that the method overrides the original on DynamicObjectProxy
                info.DeclaringType != typeof( DynamicProxy<T> ) &&
                info.GetBaseDefinition().DeclaringType == typeof( DynamicProxy<T> ) );
        }

        public override DynamicMetaObject BindGetMember( GetMemberBinder binder )
        {
            return IsOverridden( "TryGetMember" )
                 ? CallMethodWithResult( "TryGetMember", binder, NoArgs, e => binder.FallbackGetMember( this, e ) )
                 : base.BindGetMember( binder );
        }

        public override DynamicMetaObject BindSetMember( SetMemberBinder binder, DynamicMetaObject value )
        {
            return IsOverridden( "TrySetMember" )
                 ? CallMethodReturnLast( "TrySetMember", binder, GetArgs( value ), e => binder.FallbackSetMember( this, value, e ) )
                 : base.BindSetMember( binder, value );
        }

        public override DynamicMetaObject BindDeleteMember( DeleteMemberBinder binder )
        {
            return IsOverridden( "TryDeleteMember" )
                 ? CallMethodNoResult( "TryDeleteMember", binder, NoArgs, e => binder.FallbackDeleteMember( this, e ) )
                 : base.BindDeleteMember( binder );
        }


        public override DynamicMetaObject BindConvert( ConvertBinder binder )
        {
            return IsOverridden( "TryConvert" )
                 ? CallMethodWithResult( "TryConvert", binder, NoArgs, e => binder.FallbackConvert( this, e ) )
                 : base.BindConvert( binder );
        }

        public override DynamicMetaObject BindInvokeMember( InvokeMemberBinder binder, DynamicMetaObject[] args )
        {
            if ( !IsOverridden( "TryInvokeMember" ) )
                return base.BindInvokeMember( binder, args );

            //
            // Generate a tree like:
            //
            // {
            //   object result;
            //   TryInvokeMember(payload, out result)
            //      ? result
            //      : TryGetMember(payload, out result)
            //          ? FallbackInvoke(result)
            //          : fallbackResult
            // }
            //
            // Then it calls FallbackInvokeMember with this tree as the
            // "error", giving the language the option of using this
            // tree or doing .NET binding.
            //
            Fallback fallback = e => binder.FallbackInvokeMember( this, args, e );

            DynamicMetaObject call = BuildCallMethodWithResult(
                "TryInvokeMember",
                binder,
                GetArgArray( args ),
                BuildCallMethodWithResult(
                    "TryGetMember",
                    new GetBinderAdapter( binder ),
                    NoArgs,
                    fallback( null ),
                    e => binder.FallbackInvoke( e, args, null )
                ),
                null
            );

            return _dontFallbackFirst ? call : fallback( call );
        }


        public override DynamicMetaObject BindCreateInstance( CreateInstanceBinder binder, DynamicMetaObject[] args )
        {
            return IsOverridden( "TryCreateInstance" )
                 ? CallMethodWithResult( "TryCreateInstance", binder, GetArgArray( args ), e => binder.FallbackCreateInstance( this, args, e ) )
                 : base.BindCreateInstance( binder, args );
        }

        public override DynamicMetaObject BindInvoke( InvokeBinder binder, DynamicMetaObject[] args )
        {
            return IsOverridden( "TryInvoke" )
              ? CallMethodWithResult( "TryInvoke", binder, GetArgArray( args ), e => binder.FallbackInvoke( this, args, e ) )
              : base.BindInvoke( binder, args );
        }

        public override DynamicMetaObject BindBinaryOperation( BinaryOperationBinder binder, DynamicMetaObject arg )
        {
            return IsOverridden( "TryBinaryOperation" )
              ? CallMethodWithResult( "TryBinaryOperation", binder, GetArgs( arg ), e => binder.FallbackBinaryOperation( this, arg, e ) )
              : base.BindBinaryOperation( binder, arg );
        }

        public override DynamicMetaObject BindUnaryOperation( UnaryOperationBinder binder )
        {
            return IsOverridden( "TryUnaryOperation" )
                 ? CallMethodWithResult( "TryUnaryOperation", binder, NoArgs, e => binder.FallbackUnaryOperation( this, e ) )
                 : base.BindUnaryOperation( binder );
        }

        public override DynamicMetaObject BindGetIndex( GetIndexBinder binder, DynamicMetaObject[] indexes )
        {
            return IsOverridden( "TryGetIndex" )
                 ? CallMethodWithResult( "TryGetIndex", binder, GetArgArray( indexes ), e => binder.FallbackGetIndex( this, indexes, e ) )
                 : base.BindGetIndex( binder, indexes );
        }

        public override DynamicMetaObject BindSetIndex( SetIndexBinder binder, DynamicMetaObject[] indexes, DynamicMetaObject value )
        {
            return IsOverridden( "TrySetIndex" )
                 ? CallMethodReturnLast( "TrySetIndex", binder, GetArgArray( indexes, value ), e => binder.FallbackSetIndex( this, indexes, value, e ) )
                 : base.BindSetIndex( binder, indexes, value );
        }

        public override DynamicMetaObject BindDeleteIndex( DeleteIndexBinder binder, DynamicMetaObject[] indexes )
        {
            return IsOverridden( "TryDeleteIndex" )
                 ? CallMethodNoResult( "TryDeleteIndex", binder, GetArgArray( indexes ), e => binder.FallbackDeleteIndex( this, indexes, e ) )
                 : base.BindDeleteIndex( binder, indexes );
        }

        private delegate DynamicMetaObject Fallback( DynamicMetaObject errorSuggestion );

        private readonly static Expression[] NoArgs = new Expression[ 0 ];

        private static Expression[] GetArgs( params DynamicMetaObject[] args )
        {
            return args.Select( arg => Expression.Convert( arg.Expression, typeof( object ) ) ).ToArray();
        }

        private static Expression[] GetArgArray( DynamicMetaObject[] args )
        {
            return new[] { Expression.NewArrayInit( typeof( object ), GetArgs( args ) ) };
        }

        private static Expression[] GetArgArray( DynamicMetaObject[] args, DynamicMetaObject value )
        {
            return new Expression[]
            {
                Expression.NewArrayInit(typeof(object), GetArgs(args)),
                Expression.Convert(value.Expression, typeof(object))
            };
        }

        private static ConstantExpression Constant( DynamicMetaObjectBinder binder )
        {
            Type t = binder.GetType();
            while ( !t.IsVisible )
                t = t.BaseType;
            return Expression.Constant( binder, t );
        }

        /// <summary>
        /// Helper method for generating a MetaObject which calls a
        /// specific method on Dynamic that returns a result
        /// </summary>
        private DynamicMetaObject CallMethodWithResult( string methodName, DynamicMetaObjectBinder binder, Expression[] args, Fallback fallback, Fallback fallbackInvoke = null )
        {
            //
            // First, call fallback to do default binding
            // This produces either an error or a call to a .NET member
            //
            DynamicMetaObject fallbackResult = fallback( null );

            DynamicMetaObject callDynamic = BuildCallMethodWithResult( methodName, binder, args, fallbackResult, fallbackInvoke );

            //
            // Now, call fallback again using our new MO as the error
            // When we do this, one of two things can happen:
            //   1. Binding will succeed, and it will ignore our call to
            //      the dynamic method, OR
            //   2. Binding will fail, and it will use the MO we created
            //      above.
            //

            return _dontFallbackFirst ? callDynamic : fallback( callDynamic );
        }

        private DynamicMetaObject BuildCallMethodWithResult( string methodName, DynamicMetaObjectBinder binder, Expression[] args, DynamicMetaObject fallbackResult, Fallback fallbackInvoke )
        {
            //
            // Build a new expression like:
            // {
            //   object result;
            //   TryGetMember(payload, out result) ? fallbackInvoke(result) : fallbackResult
            // }
            //
            ParameterExpression result = Expression.Parameter( typeof( object ), null );

            IList<Expression> callArgs = new List<Expression>();
            callArgs.Add( Expression.Convert( Expression, typeof( T ) ) );
            callArgs.Add( Constant( binder ) );
            foreach ( var expression in args )
            {
                callArgs.Add( expression );
            }
            callArgs.Add( result );

            DynamicMetaObject resultMO = new DynamicMetaObject( result, BindingRestrictions.Empty );

            // Need to add a conversion if calling TryConvert
            if ( binder.ReturnType != typeof( object ) )
            {
                UnaryExpression convert = Expression.Convert( resultMO.Expression, binder.ReturnType );
                // will always be a cast or unbox

                resultMO = new DynamicMetaObject( convert, resultMO.Restrictions );
            }

            if ( fallbackInvoke != null )
                resultMO = fallbackInvoke( resultMO );

            DynamicMetaObject callDynamic = new DynamicMetaObject(
              Expression.Block(
                new[] { result },
                Expression.Condition(
                  Expression.Call(
                    Expression.Constant( _proxy ),
                    typeof( DynamicProxy<T> ).GetMethod( methodName ),
                    callArgs
                    ),
                  resultMO.Expression,
                  fallbackResult.Expression,
                  binder.ReturnType
                  )
                ),
              GetRestrictions().Merge( resultMO.Restrictions ).Merge( fallbackResult.Restrictions )
              );

            return callDynamic;
        }

        /// <summary>
        /// Helper method for generating a MetaObject which calls a
        /// specific method on Dynamic, but uses one of the arguments for
        /// the result.
        /// </summary>
        private DynamicMetaObject CallMethodReturnLast( string methodName, DynamicMetaObjectBinder binder, Expression[] args, Fallback fallback )
        {
            //
            // First, call fallback to do default binding
            // This produces either an error or a call to a .NET member
            //
            DynamicMetaObject fallbackResult = fallback( null );

            //
            // Build a new expression like:
            // {
            //   object result;
            //   TrySetMember(payload, result = value) ? result : fallbackResult
            // }
            //
            ParameterExpression result = Expression.Parameter( typeof( object ), null );

            IList<Expression> callArgs = new List<Expression>();
            callArgs.Add( Expression.Convert( Expression, typeof( T ) ) );
            callArgs.Add( Constant( binder ) );
            foreach ( var expression in args )
            {
                callArgs.Add( expression );
            }
            callArgs[ args.Length + 1 ] = Expression.Assign( result, callArgs[ args.Length + 1 ] );

            DynamicMetaObject callDynamic = new DynamicMetaObject(
                Expression.Block(
                    new[] { result },
                    Expression.Condition(
                        Expression.Call(
                            Expression.Constant( _proxy ),
                            typeof( DynamicProxy<T> ).GetMethod( methodName ),
                            callArgs
                        ),
                        result,
                        fallbackResult.Expression,
                        typeof( object )
                    )
                ),
                GetRestrictions().Merge( fallbackResult.Restrictions )
            );

            //
            // Now, call fallback again using our new MO as the error
            // When we do this, one of two things can happen:
            //   1. Binding will succeed, and it will ignore our call to
            //      the dynamic method, OR
            //   2. Binding will fail, and it will use the MO we created
            //      above.
            //
            return _dontFallbackFirst ? callDynamic : fallback( callDynamic );
        }

        /// <summary>
        /// Helper method for generating a MetaObject which calls a
        /// specific method on Dynamic, but uses one of the arguments for
        /// the result.
        /// </summary>
        private DynamicMetaObject CallMethodNoResult( string methodName, DynamicMetaObjectBinder binder, Expression[] args, Fallback fallback )
        {
            //
            // First, call fallback to do default binding
            // This produces either an error or a call to a .NET member
            //
            DynamicMetaObject fallbackResult = fallback( null );

            IList<Expression> callArgs = new List<Expression>();
            callArgs.Add( Expression.Convert( Expression, typeof( T ) ) );
            callArgs.Add( Constant( binder ) );
            foreach ( var expression in args )
            {
                callArgs.Add( expression );
            }

            //
            // Build a new expression like:
            //   if (TryDeleteMember(payload)) { } else { fallbackResult }
            //
            DynamicMetaObject callDynamic = new DynamicMetaObject(
              Expression.Condition(
                Expression.Call(
                  Expression.Constant( _proxy ),
                  typeof( DynamicProxy<T> ).GetMethod( methodName ),
                  callArgs
                  ),
                Expression.Empty(),
                fallbackResult.Expression,
                typeof( void )
                ),
              GetRestrictions().Merge( fallbackResult.Restrictions )
              );

            //
            // Now, call fallback again using our new MO as the error
            // When we do this, one of two things can happen:
            //   1. Binding will succeed, and it will ignore our call to
            //      the dynamic method, OR
            //   2. Binding will fail, and it will use the MO we created
            //      above.
            //
            return _dontFallbackFirst ? callDynamic : fallback( callDynamic );
        }

        /// <summary>
        /// Returns a Restrictions object which includes our current restrictions merged
        /// with a restriction limiting our type
        /// </summary>
        private BindingRestrictions GetRestrictions()
        {
            return ( Value == null && HasValue )
                 ? BindingRestrictions.GetInstanceRestriction( Expression, null )
                 : BindingRestrictions.GetTypeRestriction( Expression, LimitType );
        }

        public override IEnumerable<string> GetDynamicMemberNames()
        {
            return _proxy.GetDynamicMemberNames( Value );
        }

        // It is okay to throw NotSupported from this binder. This object
        // is only used by DynamicObject.GetMember--it is not expected to
        // (and cannot) implement binding semantics. It is just so the DO
        // can use the Name and IgnoreCase properties.
        private sealed class GetBinderAdapter : GetMemberBinder
        {
            internal GetBinderAdapter( InvokeMemberBinder binder ) :
                base( binder.Name, binder.IgnoreCase )
            {
            }

            public override DynamicMetaObject FallbackGetMember( DynamicMetaObject target, DynamicMetaObject errorSuggestion )
            {
                throw new NotSupportedException();
            }
        }
    }
}
